/*-------------------------------------------------------------------------
 *
 * nodeIndexonlyscan.c
 *	  支持仅索引扫描的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/executor/nodeIndexonlyscan.c
 *
 *-------------------------------------------------------------------------
 */
/*
 * 接口例程
 *		ExecIndexOnlyScan			扫描索引
 *		IndexOnlyNext				检索下一个元组
 *		ExecInitIndexOnlyScan		创建并初始化状态信息。
 *		ExecReScanIndexOnlyScan		重新扫描索引关系。
 *		ExecEndIndexOnlyScan		释放所有存储。
 *		ExecIndexOnlyMarkPos		标记扫描位置。
 *		ExecIndexOnlyRestrPos		恢复扫描位置。
 *		ExecIndexOnlyScanEstimate	估算并行索引仅扫描所需的 DSM 空间
 *		ExecIndexOnlyScanInitializeDSM	初始化并行索引仅扫描的 DSM
 *		ExecIndexOnlyScanReInitializeDSM	为新扫描重新初始化 DSM
 *		ExecIndexOnlyScanInitializeWorker	在并行工作者中附加到 DSM 信息
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/relscan.h"
#include "access/tableam.h"
#include "access/tupdesc.h"
#include "access/visibilitymap.h"
#include "catalog/pg_type.h"
#include "executor/execdebug.h"
#include "executor/nodeIndexonlyscan.h"
#include "executor/nodeIndexscan.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
#include "storage/predicate.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/rel.h"


static TupleTableSlot *fc_IndexOnlyNext(IndexOnlyScanState *fc_node);
static void fc_StoreIndexTuple(IndexOnlyScanState *fc_node, TupleTableSlot *fc_slot,
							IndexTuple fc_itup, TupleDesc fc_itupdesc);


/* ----------------------------------------------------------------
 *		IndexOnlyNext
 *
 *		从 IndexOnlyScan 节点的索引中检索元组。
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_IndexOnlyNext(IndexOnlyScanState *fc_node)
{
	EState	   *fc_estate;
	ExprContext *fc_econtext;
	ScanDirection fc_direction;
	IndexScanDesc fc_scandesc;
	TupleTableSlot *fc_slot;
	ItemPointer fc_tid;

	/*
	 * 从索引扫描节点提取必要信息
	 */
	fc_estate = fc_node->ss.ps.state;
	fc_direction = fc_estate->es_direction;
	/* 如果这是整体反向扫描，则翻转方向 */
	if (ScanDirectionIsBackward(((IndexOnlyScan *) fc_node->ss.ps.plan)->indexorderdir))
	{
		if (ScanDirectionIsForward(fc_direction))
			fc_direction = BackwardScanDirection;
		else if (ScanDirectionIsBackward(fc_direction))
			fc_direction = ForwardScanDirection;
	}
	fc_scandesc = fc_node->ioss_ScanDesc;
	fc_econtext = fc_node->ss.ps.ps_ExprContext;
	fc_slot = fc_node->ss.ss_ScanTupleSlot;

	if (fc_scandesc == NULL)
	{
		/*
		 * 如果索引仅扫描不是并行的，或者我们正在串行执行计划为并行的索引仅扫描，则会到达这里。
		 */
		fc_scandesc = index_beginscan(fc_node->ss.ss_currentRelation,
								   fc_node->ioss_RelationDesc,
								   fc_estate->es_snapshot,
								   fc_node->ioss_NumScanKeys,
								   fc_node->ioss_NumOrderByKeys);

		fc_node->ioss_ScanDesc = fc_scandesc;


		/* 为索引仅扫描进行设置 */
		fc_node->ioss_ScanDesc->xs_want_itup = true;
		fc_node->ioss_VMBuffer = InvalidBuffer;

		/*
		 * 如果没有运行时键需要计算，或者它们已准备好，请继续将扫描键传递给索引 AM。
		 */
		if (fc_node->ioss_NumRuntimeKeys == 0 || fc_node->ioss_RuntimeKeysReady)
			index_rescan(fc_scandesc,
						 fc_node->ioss_ScanKeys,
						 fc_node->ioss_NumScanKeys,
						 fc_node->ioss_OrderByKeys,
						 fc_node->ioss_NumOrderByKeys);
	}

	/*
	 * 好的，现在我们有了所需的内容，获取下一个元组。
	 */
	while ((fc_tid = index_getnext_tid(fc_scandesc, fc_direction)) != NULL)
	{
		bool		fc_tuple_from_heap = false;

		CHECK_FOR_INTERRUPTS();

		
/*
		 * 如果TID引用的是所有元组对所有人都可见的堆页面，我们可以跳过堆获取。无论如何，我们将使用索引元组而不是堆元组作为数据来源。
		 *
		 * 关于内存排序效应的注意事项：visibilitymap_get_status并没有锁定可见性映射缓冲区，因此我们在此读取的结果可能略显过时。然而，它不会过时到影响结果的程度。
		 *
		 * 我们需要立即检测由于插入而清除VM位，因为该元组存在于索引页面中但不可见。此扫描读取TID（使用对索引缓冲区的共享锁）与将TID插入索引的插入操作（使用对索引缓冲区的排他锁）是串行的。因为在更新索引之前清除了VM位，而索引页面的锁定/解锁充当了完整的内存屏障，因此如果我们看到一个最近插入的TID，就可以确保看到清除的位。
		 *
		 * 删除操作不会更新索引页面（只有VACUUM会清除TID），因此删除操作清除VM位与下面的测试并没有串行化，我们可能会看到一个显著过时的值。然而，我们目前不关心删除，因为该元组仍然可见，直到删除事务提交或语句结束（如果是我们的事务）。在任何情况下，清除位后，VM缓冲区上的锁将被释放（充当写屏障）。而要让我们有一个包含删除事务（使元组不可见）的快照，我们必须在那之后获取ProcArrayLock，充当读取屏障。
		 *
		 * 为了避免需要锁定VM缓冲区，这种复杂性是值得的，因为这可能会导致显著的争用。
		 */
		if (!VM_ALL_VISIBLE(fc_scandesc->heapRelation,
							ItemPointerGetBlockNumber(fc_tid),
							&fc_node->ioss_VMBuffer))
		{
			/*
			 * 糟糕，我们必须访问堆以检查可见性。
			 */
			InstrCountTuples2(fc_node, 1);
			if (!index_fetch_heap(fc_scandesc, fc_node->ioss_TableSlot))
				continue;		/* 没有可见元组，尝试下一个索引条目 */

			ExecClearTuple(fc_node->ioss_TableSlot);

			/*
			 * 此处仅支持MVCC快照，因此在找到可见条目后不应继续跟踪HOT链。如果我们确实想允许运行这样做，我们需要保持更多状态以记住下次不要调用index_getnext_tid。
			 */
			if (fc_scandesc->xs_heap_continue)
				elog(ERROR, "non-MVCC snapshots are not supported in index-only scans");

			/*
			 * 注意：在这一点上，我们在堆页面上保持了一个引脚，如scandesc->xs_cbuf中记录的那样。我们现在可以释放该引脚，但目前不清楚这样做是否有利。下一个索引条目可能需要访问同一个堆页面。
			 */

			fc_tuple_from_heap = true;
		}

		/*
		 * 将扫描元组槽填充来自索引的数据。这可能以HeapTuple或IndexTuple格式提供。理论上，一个索引AM可能同时填充两个字段，在这种情况下我们更倾向于堆格式，因为从中填充插槽可能更便宜。
		 */
		if (fc_scandesc->xs_hitup)
		{
			/*
			 * 我们没有费心去验证提供的元组是否具有完全符合槽格式的结构，但似乎值得快速检查一下字段数量。
			 */
			Assert(fc_slot->tts_tupleDescriptor->natts ==
				   fc_scandesc->xs_hitupdesc->natts);
			ExecForceStoreHeapTuple(fc_scandesc->xs_hitup, fc_slot, false);
		}
		else if (fc_scandesc->xs_itup)
			fc_StoreIndexTuple(fc_node, fc_slot, fc_scandesc->xs_itup, fc_scandesc->xs_itupdesc);
		else
			elog(ERROR, "no data returned for index-only scan");

		/*
		 * 如果索引是丢失的，我们必须重新检查索引条件。
		 */
		if (fc_scandesc->xs_recheck)
		{
			fc_econtext->ecxt_scantuple = fc_slot;
			if (!ExecQualAndReset(fc_node->recheckqual, fc_econtext))
			{
				/* 重新检查失败，因此丢弃它并循环回去获取另一个 */
				InstrCountFiltered2(fc_node, 1);
				continue;
			}
		}

		/*
		 * 我们目前不支持重新检查ORDER BY距离。（原则上，如果索引能够支持检索原始索引值，它也应该能够产生准确的距离计算。因此，添加代码用于重新检查/重新排序是否值得仍不明确。但如果有人尝试这样做，我们至少应该抛出一个错误。）
		 */
		if (fc_scandesc->numberOfOrderBys > 0 && fc_scandesc->xs_recheckorderby)
			ereport(ERROR,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("lossy distance functions are not supported in index-only scans")));

		/*
		 * 如果我们没有访问堆，那么我们需要显式地获取一个谓词锁，就像我们访问过一样。现在我们在页面级别执行此操作。
		 */
		if (!fc_tuple_from_heap)
			PredicateLockPage(fc_scandesc->heapRelation,
							  ItemPointerGetBlockNumber(fc_tid),
							  fc_estate->es_snapshot);

		return fc_slot;
	}

	/*
	 * 如果我们到这里，意味着索引扫描失败，因此我们在扫描的末尾。
	 */
	return ExecClearTuple(fc_slot);
}

/*
 * StoreIndexTuple
 *		用索引元组中的数据填充插槽。
 *
 * 在某个时候，这可能是一般有用的功能，但现在我们在其他地方并不需要它。
 */
static void fc_StoreIndexTuple(IndexOnlyScanState *fc_node, TupleTableSlot *fc_slot,
				IndexTuple fc_itup, TupleDesc fc_itupdesc)
{
	/*
	 * 注意：我们必须在index_deform_tuple中使用AM提供的tupdesc，而不是插槽的tupdesc，以防后者的数据显示类型不同（这在btree name_ops中特别发生）。不过，它们最好有相同数量的列，并且是数据类型兼容的，这是我们不容易检查的。
	 */
	Assert(fc_slot->tts_tupleDescriptor->natts == fc_itupdesc->natts);

	ExecClearTuple(fc_slot);
	index_deform_tuple(fc_itup, fc_itupdesc, fc_slot->tts_values, fc_slot->tts_isnull);

	/*
	 * 将所有作为cstrings存储的名称列复制回一个大小为NAMEDATALEN字节的分配中。我们将此分支标记为不太可能，因为一般情况下“名称”仅用于系统目录，而这必须是在那些系统目录上的用户查询或某个其他用户表中有名称列的索引。
	 */
	if (unlikely(fc_node->ioss_NameCStringAttNums != NULL))
	{
		int			fc_attcount = fc_node->ioss_NameCStringCount;

		for (int fc_idx = 0; fc_idx < fc_attcount; fc_idx++)
		{
			int			fc_attnum = fc_node->ioss_NameCStringAttNums[fc_idx];
			Name		fc_name;

			/* 跳过空的Datums */
			if (fc_slot->tts_isnull[fc_attnum])
				continue;

			/* 分配NAMEDATALEN并将datum复制到该内存中 */
			fc_name = (Name) MemoryContextAlloc(fc_node->ss.ps.ps_ExprContext->ecxt_per_tuple_memory,
											 NAMEDATALEN);

			/* 使用namestrcpy将所有后续字节填充为零 */
			namestrcpy(fc_name, DatumGetCString(fc_slot->tts_values[fc_attnum]));
			fc_slot->tts_values[fc_attnum] = NameGetDatum(fc_name);
		}
	}

	ExecStoreVirtualTuple(fc_slot);
}

/*
 * IndexOnlyRecheck -- 访问方法例程以重新检查EvalPlanQual中的元组
 *
 * 这实际上不可能发生，因为索引无法提供CTID，而这对于任何潜在的EvalPlanQual目标关系都是必要数据。如果发生，那么EPQ代码将传递给我们错误的数据，即一个堆元组，而不是索引元组。因此抛出一个错误。
 */
static bool fc_IndexOnlyRecheck(IndexOnlyScanState *fc_node, TupleTableSlot *fc_slot)
{
	elog(ERROR, "EvalPlanQual recheck is not supported in index-only scans");
	return false;				/* 保持编译器安静 */
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyScan(node)
 * ----------------------------------------------------------------
 */
static TupleTableSlot * fc_ExecIndexOnlyScan(PlanState *fc_pstate)
{
	IndexOnlyScanState *fc_node = castNode(IndexOnlyScanState, fc_pstate);

	/*
	 * 如果我们有运行时键，并且尚未设置，则现在进行设置。
	 */
	if (fc_node->ioss_NumRuntimeKeys != 0 && !fc_node->ioss_RuntimeKeysReady)
		ExecReScan((PlanState *) fc_node);

	return ExecScan(&fc_node->ss,
					(ExecScanAccessMtd) fc_IndexOnlyNext,
					(ExecScanRecheckMtd) fc_IndexOnlyRecheck);
}

/* ----------------------------------------------------------------
 *		ExecReScanIndexOnlyScan(node)
 *
 *		重新计算任何扫描键的值，其中的值依赖于运行时已知的信息，然后重新扫描索引关系。
 *
 *		更新扫描键以前是在ExecUpdateIndexScanKeys中单独完成的。将其整合到ReScan中使索引和关系/通用流的重新扫描更加统一。
 * ----------------------------------------------------------------
 */
void ExecReScanIndexOnlyScan(IndexOnlyScanState *fc_node)
{
	/*
	 * 如果我们正在进行运行时键计算（即，任何索引键值都不是简单的常量），则计算新的键值。但是首先，重置上下文，以便在扫描每个外部元组时不会泄漏内存。请注意，这假定我们将在每次调用时重新计算*所有*运行时键。
	 */
	if (fc_node->ioss_NumRuntimeKeys != 0)
	{
		ExprContext *fc_econtext = fc_node->ioss_RuntimeContext;

		ResetExprContext(fc_econtext);
		ExecIndexEvalRuntimeKeys(fc_econtext,
								 fc_node->ioss_RuntimeKeys,
								 fc_node->ioss_NumRuntimeKeys);
	}
	fc_node->ioss_RuntimeKeysReady = true;

	/* 重置索引扫描 */
	if (fc_node->ioss_ScanDesc)
		index_rescan(fc_node->ioss_ScanDesc,
					 fc_node->ioss_ScanKeys, fc_node->ioss_NumScanKeys,
					 fc_node->ioss_OrderByKeys, fc_node->ioss_NumOrderByKeys);

	ExecScanReScan(&fc_node->ss);
}


/* ----------------------------------------------------------------
 *		ExecEndIndexOnlyScan
 * ----------------------------------------------------------------
 */
void ExecEndIndexOnlyScan(IndexOnlyScanState *fc_node)
{
	Relation	fc_indexRelationDesc;
	IndexScanDesc fc_indexScanDesc;

	/*
	 * 从节点中提取信息
	 */
	fc_indexRelationDesc = fc_node->ioss_RelationDesc;
	fc_indexScanDesc = fc_node->ioss_ScanDesc;

	/* 释放VM缓冲区引脚（如果有）。 */
	if (fc_node->ioss_VMBuffer != InvalidBuffer)
	{
		ReleaseBuffer(fc_node->ioss_VMBuffer);
		fc_node->ioss_VMBuffer = InvalidBuffer;
	}

	/*
	 * 释放exprcontext（们）……现在是死代码，请参见ExecFreeExprContext
	 */
#ifdef NOT_USED
	ExecFreeExprContext(&fc_node->ss.ps);
	if (fc_node->ioss_RuntimeContext)
		FreeExprContext(fc_node->ioss_RuntimeContext, true);
#endif

	/*
	 * 清空元组表插槽
	 */
	if (fc_node->ss.ps.ps_ResultTupleSlot)
		ExecClearTuple(fc_node->ss.ps.ps_ResultTupleSlot);
	ExecClearTuple(fc_node->ss.ss_ScanTupleSlot);

	/*
	 * 关闭索引关系（如果我们没有打开它，则无操作）
	 */
	if (fc_indexScanDesc)
		index_endscan(fc_indexScanDesc);
	if (fc_indexRelationDesc)
		index_close(fc_indexRelationDesc, NoLock);
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyMarkPos
 *
 * 注意：我们假设没有调用者在读取至少一个元组之前尝试设置标记。否则，ioss_ScanDesc可能仍为NULL。
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyMarkPos(IndexOnlyScanState *fc_node)
{
	EState	   *fc_estate = fc_node->ss.ps.state;
	EPQState   *fc_epqstate = fc_estate->es_epq_active;

	if (fc_epqstate != NULL)
	{
		/*
		 * 我们在EvalPlanQual重新检查中。如果该关系存在测试元组，则我们根本不应该访问索引。我们需要保存并稍后恢复relsubs_done标志的状态，以便能够重新获取测试元组。然而，考虑到没有调用者在扫描开始时设置标记的假设，我们仅在relsubs_done[i]已经设置时才能到达这里，因此无需保存状态。
		 */
		Index		fc_scanrelid = ((Scan *) fc_node->ss.ps.plan)->scanrelid;

		Assert(fc_scanrelid > 0);
		if (fc_epqstate->relsubs_slot[fc_scanrelid - 1] != NULL ||
			fc_epqstate->relsubs_rowmark[fc_scanrelid - 1] != NULL)
		{
			/* 验证上述声明 */
			if (!fc_epqstate->relsubs_done[fc_scanrelid - 1])
				elog(ERROR, "unexpected ExecIndexOnlyMarkPos call in EPQ recheck");
			return;
		}
	}

	index_markpos(fc_node->ioss_ScanDesc);
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyRestrPos
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyRestrPos(IndexOnlyScanState *fc_node)
{
	EState	   *fc_estate = fc_node->ss.ps.state;
	EPQState   *fc_epqstate = fc_estate->es_epq_active;

	if (fc_estate->es_epq_active != NULL)
	{
		/* 请参阅ExecIndexMarkPos中的注释 */
		Index		fc_scanrelid = ((Scan *) fc_node->ss.ps.plan)->scanrelid;

		Assert(fc_scanrelid > 0);
		if (fc_epqstate->relsubs_slot[fc_scanrelid - 1] != NULL ||
			fc_epqstate->relsubs_rowmark[fc_scanrelid - 1] != NULL)
		{
			/* 验证上述声明 */
			if (!fc_epqstate->relsubs_done[fc_scanrelid - 1])
				elog(ERROR, "unexpected ExecIndexOnlyRestrPos call in EPQ recheck");
			return;
		}
	}

	index_restrpos(fc_node->ioss_ScanDesc);
}


/* ----------------------------------------------------------------
 *		ExecInitIndexOnlyScan
 *
 *		初始化索引扫描的状态信息，创建扫描键，并打开基础和索引关系。
 *
 *		注意：索引扫描有2组状态信息，因为我们必须跟踪基础关系和
 *			  索引关系。
 * ----------------------------------------------------------------
 */
IndexOnlyScanState *
ExecInitIndexOnlyScan(IndexOnlyScan *fc_node, EState *fc_estate, int fc_eflags)
{
	IndexOnlyScanState *fc_indexstate;
	Relation	fc_currentRelation;
	Relation	fc_indexRelation;
	LOCKMODE	fc_lockmode;
	TupleDesc	fc_tupDesc;
	int			fc_indnkeyatts;
	int			fc_namecount;

	/*
	 * 创建状态结构
	 */
	fc_indexstate = makeNode(IndexOnlyScanState);
	fc_indexstate->ss.ps.plan = (Plan *) fc_node;
	fc_indexstate->ss.ps.state = fc_estate;
	fc_indexstate->ss.ps.ExecProcNode = fc_ExecIndexOnlyScan;

	/*
	 * 杂项初始化
	 *
	 * 为节点创建表达式上下文
	 */
	ExecAssignExprContext(fc_estate, &fc_indexstate->ss.ps);

	/*
	 * 打开扫描关系
	 */
	fc_currentRelation = ExecOpenScanRelation(fc_estate, fc_node->scan.scanrelid, fc_eflags);

	fc_indexstate->ss.ss_currentRelation = fc_currentRelation;
	fc_indexstate->ss.ss_currentScanDesc = NULL;	/* 此处没有堆扫描 */

	/*
	 * 使用计划生成的 indextlist 构建扫描元组类型。我们使用这个，而不是索引的物理元组
	 * 描述符，因为后者包含存储列类型而不是原始数据项的类型。 （无论如何，返回
	 * 合适数据是 AM 的责任。）
	 */
	fc_tupDesc = ExecTypeFromTL(fc_node->indextlist);
	ExecInitScanTupleSlot(fc_estate, &fc_indexstate->ss, fc_tupDesc,
						  &TTSOpsVirtual);

	/*
	 * 我们需要另一个槽，以适合表 AM 的格式，以便在我们需要从表中提取元组以重新检查可见性时使用。
	 */
	fc_indexstate->ioss_TableSlot =
		ExecAllocTableSlot(&fc_estate->es_tupleTable,
						   RelationGetDescr(fc_currentRelation),
						   table_slot_callbacks(fc_currentRelation));

	/*
	 * 初始化结果类型和投影信息。该节点的 targetlist 将包含 varno = INDEX_VAR 的 Vars，引用扫描元组。
	 */
	ExecInitResultTypeTL(&fc_indexstate->ss.ps);
	ExecAssignScanProjectionInfoWithVarno(&fc_indexstate->ss, INDEX_VAR);

	/*
	 * 初始化子表达式
	 *
	 * 注意：我们不初始化所有的 indexorderby 表达式，只初始化对应于运行时键的
	 * 子部分（见下文）。
	 */
	fc_indexstate->ss.ps.qual =
		ExecInitQual(fc_node->scan.plan.qual, (PlanState *) fc_indexstate);
	fc_indexstate->recheckqual =
		ExecInitQual(fc_node->recheckqual, (PlanState *) fc_indexstate);

	/*
	 * 如果我们只是执行 EXPLAIN（即，不打算运行计划），则在这里停止。
	 * 这允许索引建议插件 EXPLAIN 一个包含对不存在的索引引用的计划。
	 */
	if (fc_eflags & EXEC_FLAG_EXPLAIN_ONLY)
		return fc_indexstate;

	/* 打开索引关系。 */
	fc_lockmode = exec_rt_fetch(fc_node->scan.scanrelid, fc_estate)->rellockmode;
	fc_indexRelation = index_open(fc_node->indexid, fc_lockmode);
	fc_indexstate->ioss_RelationDesc = fc_indexRelation;

	/*
	 * 初始化特定于索引的扫描状态
	 */
	fc_indexstate->ioss_RuntimeKeysReady = false;
	fc_indexstate->ioss_RuntimeKeys = NULL;
	fc_indexstate->ioss_NumRuntimeKeys = 0;

	/*
	 * 从索引条件构建索引扫描键
	 */
	ExecIndexBuildScanKeys((PlanState *) fc_indexstate,
						   fc_indexRelation,
						   fc_node->indexqual,
						   false,
						   &fc_indexstate->ioss_ScanKeys,
						   &fc_indexstate->ioss_NumScanKeys,
						   &fc_indexstate->ioss_RuntimeKeys,
						   &fc_indexstate->ioss_NumRuntimeKeys,
						   NULL,	/* 没有 ArrayKeys */  
						   NULL);

	/*
	 * 任何 ORDER BY 表达式都必须以相同的方式转换为 scankeys
	 */
	ExecIndexBuildScanKeys((PlanState *) fc_indexstate,
						   fc_indexRelation,
						   fc_node->indexorderby,
						   true,
						   &fc_indexstate->ioss_OrderByKeys,
						   &fc_indexstate->ioss_NumOrderByKeys,
						   &fc_indexstate->ioss_RuntimeKeys,
						   &fc_indexstate->ioss_NumRuntimeKeys,
						   NULL,	/* 没有 ArrayKeys */  
						   NULL);

	/*
	 * 如果我们有运行时键，我们需要一个 ExprContext 来评估它们。节点的标准上下文不够用，因为我们想要在每个元组中重置该上下文。 
	 * 所以，构建另一个上下文，就像另一个一样...
	 * -tgl 7/11/00
	 */
	if (fc_indexstate->ioss_NumRuntimeKeys != 0)
	{
		ExprContext *fc_stdecontext = fc_indexstate->ss.ps.ps_ExprContext;

		ExecAssignExprContext(fc_estate, &fc_indexstate->ss.ps);
		fc_indexstate->ioss_RuntimeContext = fc_indexstate->ss.ps.ps_ExprContext;
		fc_indexstate->ss.ps.ps_ExprContext = fc_stdecontext;
	}
	else
	{
		fc_indexstate->ioss_RuntimeContext = NULL;
	}

	fc_indexstate->ioss_NameCStringAttNums = NULL;
	fc_indnkeyatts = fc_indexRelation->rd_index->indnkeyatts;
	fc_namecount = 0;

	/*
	 * btree 的 "name" 类型使用 text_ops，这导致在索引键中存储 cstrings 而不是名称。 
	 * 在这里，我们以通用方式检测这一点，以防其他索引 AM 也想做相同的优化。
	 * 检查 opcintype 为 NAMEOID 且索引元组描述符为 CSTRINGOID 的操作类。 
	 * 如果发现任何这些，创建一个数组标记它们的索引属性编号。
	 * StoreIndexTuple() 处理将名称 Datums 复制到 NAMEDATALEN 字节分配中。
	 */

	/* 首先，计算这样的索引键的数量 */
	for (int fc_attnum = 0; fc_attnum < fc_indnkeyatts; fc_attnum++)
	{
		if (fc_indexRelation->rd_att->attrs[fc_attnum].atttypid == CSTRINGOID &&
			fc_indexRelation->rd_opcintype[fc_attnum] == NAMEOID)
			fc_namecount++;
	}

	if (fc_namecount > 0)
	{
		int			fc_idx = 0;

		/*
		 * 现在创建一个数组来标记需要从 cstring 转换为名称的键的属性编号。
		 */
		fc_indexstate->ioss_NameCStringAttNums = (AttrNumber *)
									palloc(sizeof(AttrNumber) * fc_namecount);

		for (int fc_attnum = 0; fc_attnum < fc_indnkeyatts; fc_attnum++)
		{
			if (fc_indexRelation->rd_att->attrs[fc_attnum].atttypid == CSTRINGOID &&
				fc_indexRelation->rd_opcintype[fc_attnum] == NAMEOID)
				fc_indexstate->ioss_NameCStringAttNums[fc_idx++] = (AttrNumber) fc_attnum;
		}
	}

	fc_indexstate->ioss_NameCStringCount = fc_namecount;

	/*
	 * 完成所有操作。
	 */
	return fc_indexstate;
}

/* ----------------------------------------------------------------
 *		并行索引仅扫描支持
 * ----------------------------------------------------------------
 */

/* ----------------------------------------------------------------
 *		ExecIndexOnlyScanEstimate
 *
 *		计算我们在并行查询 DSM 中所需的空间量，并通知 pcxt->estimator 关于我们的需求。
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyScanEstimate(IndexOnlyScanState *fc_node,
						  ParallelContext *fc_pcxt)
{
	EState	   *fc_estate = fc_node->ss.ps.state;

	fc_node->ioss_PscanLen = index_parallelscan_estimate(fc_node->ioss_RelationDesc,
													  fc_estate->es_snapshot);
	shm_toc_estimate_chunk(&fc_pcxt->estimator, fc_node->ioss_PscanLen);
	shm_toc_estimate_keys(&fc_pcxt->estimator, 1);
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyScanInitializeDSM
 *
 *		设置并行索引仅扫描描述符。
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyScanInitializeDSM(IndexOnlyScanState *fc_node,
							   ParallelContext *fc_pcxt)
{
	EState	   *fc_estate = fc_node->ss.ps.state;
	ParallelIndexScanDesc fc_piscan;

	fc_piscan = shm_toc_allocate(fc_pcxt->toc, fc_node->ioss_PscanLen);
	index_parallelscan_initialize(fc_node->ss.ss_currentRelation,
								  fc_node->ioss_RelationDesc,
								  fc_estate->es_snapshot,
								  fc_piscan);
	shm_toc_insert(fc_pcxt->toc, fc_node->ss.ps.plan->plan_node_id, fc_piscan);
	fc_node->ioss_ScanDesc =
		index_beginscan_parallel(fc_node->ss.ss_currentRelation,
								 fc_node->ioss_RelationDesc,
								 fc_node->ioss_NumScanKeys,
								 fc_node->ioss_NumOrderByKeys,
								 fc_piscan);
	fc_node->ioss_ScanDesc->xs_want_itup = true;
	fc_node->ioss_VMBuffer = InvalidBuffer;

	/*
	 * 如果没有需要计算的运行时键，或者它们已经准备好，继续将 scankeys 传递给索引 AM。
	 */
	if (fc_node->ioss_NumRuntimeKeys == 0 || fc_node->ioss_RuntimeKeysReady)
		index_rescan(fc_node->ioss_ScanDesc,
					 fc_node->ioss_ScanKeys, fc_node->ioss_NumScanKeys,
					 fc_node->ioss_OrderByKeys, fc_node->ioss_NumOrderByKeys);
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyScanReInitializeDSM
 *
 *		在开始新的扫描之前重置共享状态。
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyScanReInitializeDSM(IndexOnlyScanState *fc_node,
								 ParallelContext *fc_pcxt)
{
	index_parallelrescan(fc_node->ioss_ScanDesc);
}

/* ----------------------------------------------------------------
 *		ExecIndexOnlyScanInitializeWorker
 *
 *		从 TOC 复制相关信息到计划状态。
 * ----------------------------------------------------------------
 */
void ExecIndexOnlyScanInitializeWorker(IndexOnlyScanState *fc_node,
								  ParallelWorkerContext *fc_pwcxt)
{
	ParallelIndexScanDesc fc_piscan;

	fc_piscan = shm_toc_lookup(fc_pwcxt->toc, fc_node->ss.ps.plan->plan_node_id, false);
	fc_node->ioss_ScanDesc =
		index_beginscan_parallel(fc_node->ss.ss_currentRelation,
								 fc_node->ioss_RelationDesc,
								 fc_node->ioss_NumScanKeys,
								 fc_node->ioss_NumOrderByKeys,
								 fc_piscan);
	fc_node->ioss_ScanDesc->xs_want_itup = true;

	/*
	 * 如果没有需要计算的运行时键，或者它们已经准备好，继续将 scankeys 传递给索引 AM。
	 */
	if (fc_node->ioss_NumRuntimeKeys == 0 || fc_node->ioss_RuntimeKeysReady)
		index_rescan(fc_node->ioss_ScanDesc,
					 fc_node->ioss_ScanKeys, fc_node->ioss_NumScanKeys,
					 fc_node->ioss_OrderByKeys, fc_node->ioss_NumOrderByKeys);
}
